﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace UDS.DB.Lib
{
    internal delegate void ObjectVisited(string name);

    /// <summary>
    /// Resolves the object references.
    /// </summary>
    internal partial class ObjectDependencies
    {
        #region Inner class ObjectDepNode

        private class Node
        {
            public readonly List<Node> RefTables = new List<Node>();
            public string Name;
            public bool Printed;
            public bool Visited;
        }

        #endregion Inner class ObjectDepNode

        private readonly Dictionary<string, Node> _map;

        /// <summary>
        /// Constructor.
        /// </summary>
        private ObjectDependencies()
        {
            _map = new Dictionary<string, Node>(StringComparer.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Defines the dependency relationship between <paramref name="name"/> and <paramref name="dependencies"/>.
        /// That is, <b>name</b> depends on <b>dependencies</b>.
        /// </summary>
        private void AddDependencies(string name, IList<string> dependencies)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            if (dependencies == null)
            {
                throw new ArgumentNullException("dependencies");
            }

            // Get the node for the specified object.
            Node objectNode;
            if (!_map.TryGetValue(name, out objectNode))
            {
                objectNode = new Node();
                objectNode.Name = name;

                _map.Add(name, objectNode);
            }

            // Add dependencies for the object.
            foreach (string dep in dependencies)
            {
                if (string.IsNullOrEmpty(dep))
                {
                    throw new ArgumentNullException("dependencies");
                }

                Node refNode;
                if (!_map.TryGetValue(dep, out refNode))
                {
                    refNode = new Node();
                    refNode.Name = dep;

                    _map.Add(dep, refNode);
                }

                objectNode.RefTables.Add(refNode);
            }
        }

        internal void VisitObjects(ObjectVisited callback)
        {
            if (callback == null)
            {
                throw new ArgumentNullException("callback");
            }

            while (true)
            {
                Node t = FindOne();
                if (t == null)
                {
                    break;
                }

                if (!VisitObjects(t, callback))
                {
                    throw new InvalidOperationException("Why not printed?");
                }
            }
        }

        private bool VisitObjects(Node t, ObjectVisited callback)
        {
            Debug.Assert(t != null);
            Debug.Assert(callback != null);

            if (t.Visited)
            {
                throw new InvalidOperationException("cycle reference?");
            }
            t.Visited = true;
            if (t.RefTables.Count == 0)
            {
                callback(t.Name);
                t.Printed = true;
                return true;
            }

            // Check to see whether the referenced tables can be printed?
            foreach (Node refNode in t.RefTables)
            {
                if (refNode == t)
                {
                    continue;
                }

                if (refNode.Visited && CanNavigate(refNode, t))
                {
                    throw new InvalidOperationException("cycle reference?");
                }

                if (!refNode.Printed)
                {
                    return VisitObjects(refNode, callback);
                }
                refNode.Visited = true;
            }

            // As all the referenced tables have already been printed, the current table can be printed now
            if (!t.Printed)
            {
                callback(t.Name);
                t.Printed = true;
                return true;
            }

            return t.Printed;
        }

        private Node FindOne()
        {
            foreach (var kvp in _map)
            {
                kvp.Value.Visited = false;
            }

            foreach (var kvp in _map)
            {
                if (!kvp.Value.Printed)
                {
                    return kvp.Value;
                }
            }
            return null;
        }

        private static bool CanNavigate(Node from, Node to)
        {
            Debug.Assert(from != null);
            Debug.Assert(to != null);
            Debug.Assert(from != to);

            if (from.RefTables.Count == 0)
            {
                return false;
            }

            foreach (Node refNode in from.RefTables)
            {
                if (ReferenceEquals(refNode, to))
                {
                    return true;
                }

                if (CanNavigate(refNode, to))
                {
                    return true;
                }
            }

            return false;
        }
    }
}